/**   
 * Copyright 2011 COMERE Harold
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package etrl.core;

import java.util.ArrayList;
import java.util.Iterator;

import etrl.camera.Camera;
import etrl.shaders.DynamicShaderProgram;
import etrl.vecmath.Matrix3f;
import etrl.vecmath.Matrix4f;
import etrl.vecmath.Vector3f;
import etrl.vecmath.Vector4f;

/**
 * \class RenderVisitor
 * \brief Specialized node visitor doing scene graph rendering.
 * @author harold
 *
 */
public class RenderVisitor implements NodeVisitor
{
	/**
	 * Stack of defined shader programs while traversing the graph. The last
	 * defined shader program will be used when the visitor reach a geode
	 */
	private ArrayList<DynamicShaderProgram> _shaderProgramStack;

	/**
	 * Stack of transformations found while traversing. Similar to old
	 * glPushMatrix and glPopMatrix Used to compute modelMatrix at Geode level
	 */
	private ArrayList<TransformData> _transformDataStack;
	
	/**
	 * Stack of textures to activate found while traversing.
	 * At geode stade, all textures of this stack will be used
	 */
	private ArrayList <TextureLayer> _textureStack;
	
	/**
	 * Set of lights source name found while traversing
	 */
	private ArrayList <String> _lightSourceStack;
	
	/**
	 * Record the last activated shader program to avoid useless re-activation
	 */
	private DynamicShaderProgram _shaderProgramActivated;

	/**
	 * Camera view matrix stack.
	 * The stack contains initialy the main camera view matrix
	 * and the visitor will push other view matrix of found 
	 * cameras while visiting.
	 */
	private ArrayList <Matrix4f> _viewMatrixStack;

	/**
	 * Camera projection matrix stack.
	 * The stack contains initialy the main camera projection matrix
	 * and the visitor will push other projection matrix of found 
	 * cameras while visiting.
	 */
	private ArrayList <Matrix4f> _projectionMatrixStack;
	
	/**
	 * Current computed model view matrix after reaching a geode
	 */
	private Matrix4f _modelViewMatrix;
	
	/**
	 * Current computed normal matrix after reaching a geode
	 */
	private Matrix3f _normalMatrix;
	
	/**
	 * Computed light data array at geode stade
	 */
	private float[] _lightData;
	
	/**
	 * Computed light color array at geode stade
	 */
	private float[] _lightColor;

	/**
	 * \brief Initializes the visitor.
	 */
	public RenderVisitor()
	{
		// Instantiate the view and projection matrix stack
		_viewMatrixStack = new ArrayList <Matrix4f>();
		_projectionMatrixStack = new ArrayList <Matrix4f>();

		// Instanciate stacks
		_shaderProgramStack = new ArrayList<DynamicShaderProgram>();
		_transformDataStack = new ArrayList<TransformData>();
		_textureStack = new ArrayList <TextureLayer>();
		_lightSourceStack = new ArrayList <String>();

		// Initialize the transform data stack with identity transform
		TransformData identity = new TransformData();
		_transformDataStack.add(identity);

		// No current activated shader program
		_shaderProgramActivated = null;
	}
	
	/**
	 * {@inheritDoc}
	 */
	
	public void visit(Geode geode)
	{
		//System.out.println("visit geode : " + geode.getName());

		// Here we are on a leaf, so its time to activate the shader program
		// before render geometries
		// If the geode has it own shader program, activate it, else use the
		// last one in the shaderProgramStack
		DynamicShaderProgram programToActivate = null;

		if (geode.getShaderProgram() != null)
			programToActivate = geode.getShaderProgram();
		else
		{
			// Btw if the shaderProgramStack is empty we wont activate any
			// program ...
			// Else the last element is at index = size - 1
			if (_shaderProgramStack.size() > 0)
				programToActivate = _shaderProgramStack.get(_shaderProgramStack.size() - 1);
		}

		// Now compare the program to activate with the current activated
		// If different, activate it, else it is already activated, save an
		// useless activation
		// But if the shader to activate is null, it means no shader program has
		// been defined, so do nothing
		if (programToActivate != null && _shaderProgramActivated != programToActivate)
		{
			// Activate the shader program
			programToActivate.activate(true);

			// Keep it at current one
			_shaderProgramActivated = programToActivate;
		}

		// Compute modelview and normal matrice
		_modelViewMatrix = new Matrix4f(_viewMatrixStack.get(_viewMatrixStack.size() - 1));
		_modelViewMatrix.mul(_transformDataStack.get(_transformDataStack.size() - 1).toMat4());

		_normalMatrix = new Matrix3f();
		_modelViewMatrix.getRotationScale(_normalMatrix);
		_normalMatrix.invert();

		_modelViewMatrix.transpose();
		
		// Stack the geode textures
		int textureCount = geode.getTextureCount();
		if(textureCount > 0)
		{
			ArrayList <TextureLayer> textures = geode.getTextures();
			for(Iterator <TextureLayer> it = textures.iterator(); it.hasNext();)
			{
				_textureStack.add(it.next());
			}
		}
				
		// We can only use 8 texture units as maximum
		int unit = 0;
		Iterator<TextureLayer> it = _textureStack.iterator();
		while (it.hasNext() && unit < 8)
		{
			// Activate the texture layer for current unit
			it.next().activate(unit);
						
			// And go to next texture unit
			++unit;
		}
		
		// There is no limit about light number, lets build light data array and light color array
		// In case where there is no light activated, set to null
		if(_lightSourceStack.size() == 0)
		{
			_lightData = null;
			_lightColor = null;			
		}
		else
		{			
			// Else we can build arrays ... first allocate good arrays size
			_lightData = new float[4 * _lightSourceStack.size()];
			_lightColor = new float[3 * _lightSourceStack.size()];
		
			int lightNum = 0;
			Iterator <String> itlight = _lightSourceStack.iterator();
			while(itlight.hasNext())
			{
				String lightName = itlight.next();
				
				// As the light data is defined as relative, we must transform it with the current modelview matrix
				Vector4f data = LightStore.getInstance().getLightSourceData(lightName);
				_viewMatrixStack.get(_viewMatrixStack.size() - 1).transform(data);
				
				_lightData[4 * lightNum] 		= data.x;
				_lightData[4 * lightNum + 1]	= data.y;
				_lightData[4 * lightNum + 2]	= data.z;
				_lightData[4 * lightNum + 3]	= data.w;
				
				Vector3f color = LightStore.getInstance().getLightSourceColor(lightName);
				_lightColor[3 * lightNum]		= color.x;
				_lightColor[3 * lightNum + 1]	= color.y;
				_lightColor[3 * lightNum + 2]	= color.z;
				
				++lightNum;
			}
		}
					
		// Before rendering, ask the activated shader program to initialize the uniforms data
		_shaderProgramActivated.doUniformBinding(this);
		
		// Now we are ready to render geometries stored in the geode !
		geode.render();
		
		// Unstack texture after rendering
		if(textureCount > 0)
		{
			ArrayList <TextureLayer> textures = geode.getTextures();
			for(it = textures.iterator(); it.hasNext();)
			{
				_textureStack.remove(it.next());
			}			
		}
	
		// After end of rendering call the node stuff
		visit((Node) geode);
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void visit(Group group)
	{
		//System.out.println("visit group : " + group.getName());

		// First push the node's shader program if it has one
		DynamicShaderProgram program = group.getShaderProgram();
		if (program != null)
			_shaderProgramStack.add(program);
		
		// Then heap up the node's textures
		int textureCount = group.getTextureCount();
		if(textureCount > 0)
		{
			ArrayList <TextureLayer> textures = group.getTextures();
			for(Iterator <TextureLayer> it = textures.iterator(); it.hasNext();)
			{
				_textureStack.add(it.next());
			}
		}
				
		// Now continue traversing with the group's childs
		group.traverse(this);
		
		// After child traversing, call the node stuff
		visit((Node) group);
		
		// Unstack textures
		if(textureCount > 0)
		{
			ArrayList <TextureLayer> textures = group.getTextures();
			for(Iterator <TextureLayer> it = textures.iterator(); it.hasNext();)
			{
				_textureStack.remove(it.next());
			}			
		}
		
		// And finally unstack program shader
		if (program != null)
			_shaderProgramStack.remove(_shaderProgramStack.size() - 1);
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void visit(Transform transform)
	{
		//System.out.println("visit transform : " + transform.getName());

		// The first thing to do is to add to transform stack the transform data
		// Clone the last transform data in the stack, apply the Transform
		// transformation
		// and add it in stack as current transformation
		TransformData transformData = _transformDataStack.get(_transformDataStack.size() - 1).clone();
		transformData.apply(transform.getData());
		_transformDataStack.add(transformData);
		
		// Then call the usual visit call for groups
		visit((Group) transform);

		// Now we can pop the transform data
		_transformDataStack.remove(_transformDataStack.size() - 1);
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void visit(Lights lights)
	{
		// Stack the geode attached lights
		int lightSourceCount = lights.getLightSourceCount();
		if(lightSourceCount > 0)
		{
			ArrayList <String> lightNames = lights.getLightSources();
			for(Iterator <String> itlight = lightNames.iterator(); itlight.hasNext();)
			{
				_lightSourceStack.add(itlight.next());
			}
		}
		
		// Then call the usual visit call for groups
		visit((Group) lights);
		
		// And unstack lights
		if(lightSourceCount > 0)
		{
			ArrayList <String> lightNames = lights.getLightSources();
			for(Iterator <String> itlight = lightNames.iterator(); itlight.hasNext();)
			{
				_lightSourceStack.remove(itlight.next());
			}
		}
		
	}
	
	/**
	 * {@inheritDoc}
	 */
	
	public void visit(Node node)
	{
		// if the node has a node callback, call it
		if(node.getNodeCallback() != null)
			node.getNodeCallback().doIt();
	}
	
	/**
	 * {@inheritDoc}
	 */
	
	public void visit(Camera camera)
	{
		// When we find a camera, stack its projection matrix
		// and use its view matrix as absolute
			
		// So push the projection and view matrix
		_projectionMatrixStack.add(camera.getProjectionMatrix());
		_viewMatrixStack.add(camera.getInverseViewMatrix());
		
		// Continue traversal in group
		visit((Group) camera);
		
		// And pop the projection and view matrix
		_projectionMatrixStack.remove(_projectionMatrixStack.size() - 1);
		_viewMatrixStack.remove(_viewMatrixStack.size() - 1);
	}
	
	/**
	 * \brief Gets the modelview matrix at the current traversing state.
	 * @return Modelview matrix.
	 */
	public Matrix4f getModelViewMatrix()
	{
		return _modelViewMatrix;
	}
	
	/**
	 * \brief Gets the normal matrix at the current traversing state.
	 * @return Normal matrix.
	 */
	public Matrix3f getNormalMatrix()
	{
		return _normalMatrix;
	}
	
	/**
	 * \brief Gets the projection matrix at the current traversing state.
	 * @return Projection matrix.
	 */
	public Matrix4f getProjectionMatrix()
	{
		// The current active projection matrix is the last one on the stack
		return _projectionMatrixStack.get(_projectionMatrixStack.size() - 1);
	}
	
	/**
	 * \brief Gets the texture matrix of the requested texture unit, at the current traversing state.
	 * @param[in] unit Texture unit to check.
	 * @return Texture matrix.
	 */
	public Matrix4f getTextureMatrix(int unit)
	{
		// If the current texture layer stack does not have enough elements to reach the requested unit, error ...
		if(_textureStack.size() >= unit + 1)
		{
			return _textureStack.get(unit).getTextureMatrix();
		}
		else
		{
			System.out.println("[RenderVisitor::getTextureMatrix(" + unit + ")] Error : there is not enough texture layer activated to reach requested unit ... returning identity");
			return new Matrix4f(1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f);
		}
	}
	
	/**
	 * \brief Gets the number of activated textures at the current traversing state.
	 * @return Activated textures count.
	 */
	public int getTextureCount()
	{
		return _textureStack.size();
	}
	
	/**
	 * \brief Gets the data of activated lights at the current traversing state.
	 * @return Light datas vector.
	 */
	public float[] getLightData()
	{
		return _lightData;
	}
	
	/**
	 * \brief Gets the color of activated lights at the current traversing state.
	 * @return Light colors vector.
	 */
	public float[] getLightColor()
	{
		return _lightColor;
	}
	
	/**
	 * \brief Gets the number of activated lights at the current traversing state.
	 * @return Activated lights count.
	 */
	public int getLightCount()
	{
		return _lightSourceStack.size();
	}

}
